package pl.edu.mimuw.irs.core.model.analysis.resources;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import pl.edu.mimuw.irs.core.global.IDManager;
import pl.edu.mimuw.irs.core.model.analysis.dd.ActivityWrapper;
import pl.edu.mimuw.irs.core.model.analysis.exception.InternalAnalysisException;
import pl.edu.mimuw.irs.core.model.analysis.exception.InternalAnalysisException.AnalysisCause;
import pl.edu.mimuw.irs.core.model.design.resource.Requirement;
import pl.edu.mimuw.irs.core.model.design.resource.Resource;
import pl.edu.mimuw.irs.core.model.design.resource.Role;
import pl.edu.mimuw.irs.core.model.petrinet.ITransition;
import pl.edu.mimuw.irs.core.model.petrinet.Place;
import pl.edu.mimuw.irs.core.util.Position;

public class ResourcePlace extends Place {
	
	private static final long serialVersionUID = 1363505382837136707L;
	
	// *** Attributes
	
	private Resource resource;
	private Set<RequirementPlace> metRequirements;
	private Set<RequirementPlace> cachedMet;
	private Set<Role> availableRoles;
	private ITransition hire;
	private ITransition fire;
	private Map<ActivityWrapper, Set<RequirementPlace>> membership;
	public Resource getResource() {
		return resource;
	}
	public Set<RequirementPlace> getMetRequirements() {
		if (metRequirements == null) metRequirements = new LinkedHashSet<RequirementPlace>();
		if (getMembership().size() == 0) return metRequirements;
		/* 
		 * Here we know that this resource is used in some activity, so 
		 * we have to return newly created map of requirements currently met
		 * by this resource.
		 */
		if (cachedMet == null) cachedMet = getMetRequirements(metRequirements);
		return cachedMet;
	}
	public void clearCache() {
		cachedMet = null;
	}
	public Set<Role> getAvailableRoles() {
		if (availableRoles == null) availableRoles = new LinkedHashSet<Role>();
		return availableRoles;
	}
	public ITransition getHire() {
		return hire;
	}
	public ITransition getFire() {
		return fire;
	}
	public Map<ActivityWrapper, Set<RequirementPlace>> getMembership() {
		if (membership == null) membership = 
			new LinkedHashMap<ActivityWrapper, Set<RequirementPlace>>();
		return membership;
	}

	// *** Constructors

	public ResourcePlace(IDManager idManager, Resource resource, 
			Position position, ITransition hire, ITransition fire, int tokens) {
		super(idManager, idManager.getPrefixedNewId("Idle" + resource.getName() + "_"), 
				position, tokens);
		this.resource = resource;
		this.hire = hire;
		this.fire = fire;
		getAvailableRoles();
		for (Role role : resource.getRolesMap().keySet()) {
			availableRoles.add(role);
		}
	}

	public ResourcePlace(IDManager idManager, Resource resource, 
			ITransition hire, ITransition fire, 
			Position position) {
		this(idManager, resource, position, hire, fire, 0);
	}
	
	// *** Methods
	
	public Set<RequirementPlace> getMetRequirements(Set<RequirementPlace> requirements) {
		Set<RequirementPlace> newMetRequirements = 
			new LinkedHashSet<RequirementPlace>();
		for (RequirementPlace requirement : requirements) {
			if (meets(requirement)) {
				newMetRequirements.add(requirement);
			}
		}
		return newMetRequirements;
	}
	
	public Set<Requirement> getMetRequirements(
			Map<Requirement, Integer> requirements) {
		Set<Requirement> newMetRequirements = 
			new LinkedHashSet<Requirement>();
		for (Entry<Requirement, Integer> entry : requirements.entrySet()) {
			if ((entry.getValue() > 0) && meets(entry.getKey())) {
				newMetRequirements.add(entry.getKey());
			}
		}
		return newMetRequirements;
	}
	
	/*
	 * Changed implementation of resource to cope with being busy.
	 */
	public boolean meets(RequirementPlace requirementPlace) {
		return meets(requirementPlace.getRequirement());
	}
	
	/*
	 * The design assumes, that one requirement cannot have repeatable roles
	 * in one requirement and no repeatable requirements can be met by resource.
	 */
	public boolean meets(Requirement requirement) {
		return getAvailableRoles().containsAll(requirement.getRolesMap().keySet());
	}
	
	public void hireTo(ActivityWrapper activity, Set<RequirementPlace> requirements) {
		getAvailableRoles();
		if (getTokens() > 0) {
			if (!getHire().invoke())
				throw new InternalAnalysisException(
						AnalysisCause.RESOURCE_HIRE_UNEXPECTED_INVOKE_FAILURE, this);
		}
		getMembership().put(activity, requirements);
		activity.getInvolvedResources().put(this, requirements);
		/*
		 * We remove all roles from this resource that are required for above
		 * requirements.
		 */
		for (RequirementPlace place : requirements) {
			for (Role role : place.getRequirement().getRolesMap().keySet()) {
				if (availableRoles.contains(role)) availableRoles.remove(role);
				else 
					throw new InternalAnalysisException(AnalysisCause.RESOURCE_HIRE_NEGATIVE_ROLE);
			}
		}
		/*
		 * Clear cache because available roles changed.
		 */
		clearCache();
	}
	
	public void fire(ActivityWrapper activity) {
		getAvailableRoles();
		if (getMembership().size() == 1) {
			if (!getFire().invoke())
				throw new InternalAnalysisException(
						AnalysisCause.RESOURCE_FIRE_UNEXPECTED_INVOKE_FAILURE, this);
		}
		Set<RequirementPlace> requirements = getMembership().get(activity);
		getMembership().remove(activity);
		activity.getInvolvedResources().remove(this);
		for (RequirementPlace requirement : requirements) {
			for (Role role : requirement.getRequirement().getRolesMap().keySet()) {
				if (!availableRoles.contains(role)) availableRoles.add(role);
				else throw new InternalAnalysisException(AnalysisCause.RESOURCE_FIRE_DOUBLE_ROLE);
			}
		}
		/*
		 * Clear cache because available roles changed.
		 */
		clearCache();
	}
	
	@Override
	public String toString() {
		return "@" + getResource() + "@";
	}
}
